大多數應用程式都需要從網路上讀取資料。Dart 和 Flutter 支援 http
套件協助我們實現這類功能。
⚠️ 注意:你應避免直接使用
dart:io
或者dart:html
來發送 HTTP 請求。這些函式庫相依於特定平台,只能在單一平台上執行,例如dart:io
用於非網頁平台如 Android,而dart:html
則使用於網頁。
http
套件http
套件提供了最簡易的方式來讀取網路上的資料。首先我們使用下面指令安裝:
$ flutter pub add http
接著,匯入套件
import 'package:http/http.dart' as http;
如果要支援 Android,那麼請編輯 /android/app/src/main/AndroidManifest.xml
檔案加入網路權限。
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 加入這行 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- ... -->
</manifest>
同樣的如果需支援 macOS 則須編輯 macos/Runner/DebugProfile.entitlements
和 macos/Runner/Release.entitlements
檔案
<key>com.apple.security.network.client</key>
<true/>
下面的範例為使用 http.get()
方法 從 JSONPlaceholder 讀取相簿。
Future<http.Response> fetch() {
return http.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
}
http.get()
方法回傳 Future
型別的物件且包含了一個 Response
。
Future
是 Dart 的核心類別之一,用於實作非同步操作。一個 Future
物件代表一個尚未完成的操作,這個操作會在未來的某個時間點產生一個值或一個錯誤。它的概念類似於 JavaScript 中的 Promise。http.Response
類別包含收到的資料。雖然從上面看來,發送一個請求並不困難,但使用原始的 Future<http.Response>
整體上並不方便。為了後續更簡易的使用通常我們會把 http.Response
轉換為一個符合用途的 Dart 物件。
Album
類別首先,建立 Album
類別,使其涵蓋從請求取得且我們需要的資料。這個類別包含了一個工廠建構子,可以從 JSON 建立我們的物件。
其中一種作法就是使用 pattern matching 來轉換 JSON。更多教學可以參考 JSON 序列化
class Album {
final int userId;
final int id;
final String title;
// 常數建構子表示建立之後不可變
const Album({
required this.userId;,
required this.id,
required this.title,
});
factory Album.fromJson(Map<String, dynamic> json) {
// Dart 3.0 匹配模式新語法
return swtich (json) {
// 對於 Map 的匹配
{
'userId': int userId,
'id': int id,
'title': String title
} => Album(
userId: userId,
id: id,
title: title,
),
_=> throw const FromatException("載入失敗")
}
}
}
switch 匹配模式(Pattern Matching)
我們都知道
switch
一般用於控制流程,可以根據變數進行匹配,通過case
進行流程上的控制。Dart 3.0 之前有一個重點,那就是case
後面只能接「常量」就是編譯時已經固定的「值」如const int n = 1
。
日常開發一般來說常常用於匹配int
double
String
enum
void run(int v) { switch (v) { case 0: // 物件也可以匹配 // ... break; case 1: // ... break; } }
而 3.0 有了新的匹配模式之後,我們可以:
DateTime date = DateTime(2024, 7, 30); DateTime now = DateTime.now(); Duration diff = date.difference(DateTime(now.year, now.month, now.day)); String durationText = switch (diff) { Duration(inDays: -1) => '昨天', Duration(inDays: 0) => '今天', Duration(inDays: 1 ) => '明天', _ => DateFormat('yyyy年MM月dd日').format(date) // 無法匹配的選項 }
除了一般物件,還可以對 Map 進行匹配,這就是上面
fromJson
的語法。
http.Response
成 Album
現在,我們可以來更新我們的 fetch()
函式:
dart:convert
專案回傳的 Response Body 為 JSON Map
Map
為 Album
null
。import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
// ...
Future<Album> fetch() async {
final response = await http.get(Uri.parse("https://jsonplaceholder.typicode.com/albums/1"));
if (response.statusCode == 200) {
return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('載入失敗');
}
}
現在我們可以在 initState()
或 didChangeDependencies()
方法中呼叫我們的 fetch
。
initState()
方法只會在初始化的時候呼叫一次,之後就不會觸發了。如果希望在 InheritedWidget
發生變更時重新呼叫 API 載入,可以在 didChangeDependencies()
方法中呼叫。
class _MyAppState extends State<MyApp> {
late Future<Album> future;
@override
void initState() {
super.initState();
future = fetch();
}
// ...
}
要顯示資料可以使用 FutureBuilder
組件。FutureBuilder
組件的用途就是為了簡化顯示非同步取得的資料。
我們需要提供 2 個參數;Future
和 fetch()
函式。builder
函式會負責根據 Future
的狀態來渲染。
注意:snapshot.hasData
只有在取得資料時且不為 null
才會為 true
。
由於 fetch
可以只回傳非 null
資料,在遇到 404 的情況會拋出例外。例外會將 snapshot.hasError
設為 true
此時可以顯示錯誤訊息。
FutureBuilder<Album>(
future: future,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snpashot.data!.title);
} else if (snapshot.hasError) {
return Text(snapshot.error);
}
// 預設顯示一個載入的效果
return const CircularProgressIndicator();
}
);
雖然可以在 build()
方法中呼叫 fetch()
但實務上不推薦。因為 Flutter 隨時可能調用 build()
且這種頻率還蠻高的。如果將 fetch()
放到 build()
中很容易導致效能降低。